package lcs;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;

public class LCS implements java.io.Serializable{
	private static final long serialVersionUID = -6382748509865200559L;
	// parameters
	private final double initialFitness = 20;
	private final double discount = 0.71;
	private final double tax = 0.1;
	private final double coveringTrigger = 0.5;
	private final double ratePerStep = 0.25;
	private final double crossOver = 0.5;
	private final double mutRate = 0.002;
	private final int initialClassifierCount = 400;
	////////////////////////////////////////////////////////////////////
	
	private LinkedList<Classifier> classifiers= new LinkedList<Classifier>();
	private Random r = new Random();
	private double currentStep = 0;
	private LinkedList< LinkedList<Classifier> > lastClassifiers = new LinkedList< LinkedList<Classifier> >();
	
	public LCS()
	{
		
	}
	
	public void initialize( )
	{
		initialize(initialClassifierCount);
	}
	
	public void initialize( int classifierCount )
	{
		for( int i = 0; i < classifierCount; ++i )
		{
			State s = getRandomState();
			int action = getRandomAction();
			Classifier c = new Classifier(s, action, initialFitness);
			this.classifiers.add(c);
		}
	}
	
	public int match( State currentState ) throws Exception
	{
		if( this.classifiers.size()== 0 )
			throw new Exception("No classifiers left!!!");
		
		LinkedList<Classifier> matches = new LinkedList<Classifier>();
		int avgFitness = 0, matchFitness = 0;
		for( Classifier c: this.classifiers )
		{
			if( currentState.equals(c.getState()) )
			{
				matchFitness += c.getFitness();
				matches.add(c);
			}
			avgFitness += c.getFitness();
		}
		avgFitness /= this.classifiers.size();
		
		// check if match is poor or empty --> COVERING (S.51)
		if( matches.size() == 0 || matchFitness < (coveringTrigger*avgFitness) )
		{
			State s = getRandomState();
			int action = getRandomAction();
			Classifier c = new Classifier(s ,action, avgFitness);
			matches.add(c);
			this.classifiers.add(c);
			return c.getAction();
		}
		
		// normalize and sort
		TreeMap<Double,Classifier> roulette = new TreeMap<Double,Classifier>();
		double sum = getFitnessSum();
		for( Classifier c: this.classifiers )
		{
			roulette.put(c.getFitness() / sum, c);
		}
		// selection
		Classifier result = null;
		while( result == null )
		{
			Double key = roulette.ceilingKey( r.nextDouble() );
			if( key == null )
				continue;
			result = roulette.get(key);
		}
		int action = result.getAction();
		// we only want to have 4 buckets
		if( lastClassifiers.size() >= 4 )
			lastClassifiers.removeLast();
		// create and save a new bucket for all matches with the same action
		LinkedList<Classifier> bucket = new LinkedList<Classifier>();
		for( Classifier c: roulette.values() )
		{
			if( c.getAction() == action )
				bucket.add(c);
		}
		lastClassifiers.add(bucket);		
		return action;
	}
	
	private State getRandomState()
	{
		// cooldown [0;30]
		Integer cooldown = r.nextDouble() <= 0.3 ? null : r.nextInt(31);		
		// distance [0;3]
		Integer distance =  r.nextDouble() <= 0.3 ? null : r.nextInt(4);
		// enemies [0;3]
		Integer enemyCount =  r.nextDouble() <= 0.3 ? null : r.nextInt(4);
		// health [0;3]
		Integer healthState =  r.nextDouble() <= 0.3 ? null : r.nextInt(4);
		
		return new State(cooldown,distance,enemyCount,healthState);
	}
	
	private int getRandomAction()
	{
		return r.nextInt(2);
	}
	
	private double getFitnessSum()
	{
		double result = 0;
		for( Classifier c: this.classifiers )
		{
			result += c.getFitness();
		}
		return result;
	}
	
	public void doReward(double reward)
	{
		// S.47+
		Iterator<LinkedList<Classifier>> it = this.lastClassifiers.iterator();
		if( !it.hasNext() )
			return;
		
		LinkedList<Classifier> A = it.next();
		if( A.size() == 0 )
			return;
		
		double fraction = reward / (A.size() + 1);
		for( Classifier c: A )
		{
			c.addFitness(fraction);
		}
		// discount bucket value
		fraction *= this.discount;
		while( it.hasNext() )
		{
			LinkedList<Classifier> Aold = it.next();
			for( Classifier c: Aold )
			{
				c.addFitness(fraction);
			}
			// more taxes ~~
			fraction *= (1.0 - this.tax);
		}
	}
	
	public void step()
	{
		// invoke GA
		currentStep += this.ratePerStep;
		if( currentStep < 1.0 )
			return;
		currentStep -= 1.0;
		
		// normalize and sort
		TreeMap<Double,Classifier> roulette = new TreeMap<Double,Classifier>();
		double sum = getFitnessSum();
		for( Classifier c: this.classifiers )
		{
			roulette.put(c.getFitness() / sum, c);
		}
		// selection
		Classifier c1 = null, c2 = null;
		while( c1 == null || c2 == null )
		{
			Double key = roulette.ceilingKey( r.nextDouble() );
			if( key == null )
				continue;
			if( c1 == null )
				c1 = roulette.get(key);
			else
				c2 = roulette.get(key);
		}
		// crossover
		if( r.nextDouble() <= this.crossOver )
		{
			switch( r.nextInt(4) )
			{
			case 0: // cooldown
			{
				Integer temp = c1.getState().getCooldown();
				c1.getState().setCooldown(c2.getState().getCooldown());
				c2.getState().setCooldown(temp);
				break;
			}
			case 1: // distance
			{
				Integer temp = c1.getState().getDistanceState();
				c1.getState().setDistanceState(c2.getState().getDistanceState());
				c2.getState().setDistanceState(temp);
				break;
			}
			case 2: // enemyCount
			{
				Integer temp = c1.getState().getEnemyCount();
				c1.getState().setEnemyCount(c2.getState().getEnemyCount());
				c2.getState().setEnemyCount(temp);
				break;
			}
			case 3: // healthState
			{
				Integer temp = c1.getState().getHealthState();
				c1.getState().setHealthState(c2.getState().getHealthState());
				c2.getState().setHealthState(temp);
				break;
			}
			}
		}
		// mutate c1?
		if( r.nextDouble() < this.mutRate )
		{
			State randomState = this.getRandomState();
			switch( r.nextInt(4) )
			{
			case 0:
				c1.getState().setCooldown(randomState.getCooldown());
				break;
			case 1:
				c1.getState().setDistanceState(randomState.getDistanceState());
				break;
			case 2:
				c1.getState().setEnemyCount(randomState.getEnemyCount());
				break;
			case 3:
				c1.getState().setHealthState(randomState.getHealthState());
				break;
			}
		}
		// mutate c2?
		if( r.nextDouble() < this.mutRate )
		{
			State randomState = this.getRandomState();
			switch( r.nextInt(4) )
			{
			case 0:
				c2.getState().setCooldown(randomState.getCooldown());
				break;
			case 1:
				c2.getState().setDistanceState(randomState.getDistanceState());
				break;
			case 2:
				c2.getState().setEnemyCount(randomState.getEnemyCount());
				break;
			case 3:
				c2.getState().setHealthState(randomState.getHealthState());
				break;
			}
		}
		// kill classifier which weren't accessed, to keep a constant population
		if(  this.classifiers.size() <= this.initialClassifierCount*4 )
			return;
		SortedMap<Integer, Classifier> accessMap = new TreeMap<Integer,Classifier>();
		for( Classifier c: this.classifiers )
		{
			accessMap.put(c.getAccessed(), c);
		}
		int toRemove = this.classifiers.size() - this.initialClassifierCount*4;
		ArrayList<Classifier> temp = new ArrayList<Classifier>(toRemove);
		
		int i = 0;
		for( Classifier c : accessMap.values() )
		{
			temp.add(c);
			++i;
			if( i >= toRemove )
				break;
		}
		this.classifiers.removeAll(temp);
	}
	
	public void save()
	{
		try (
		      OutputStream file = new FileOutputStream("LCS.obj");
		      OutputStream buffer = new BufferedOutputStream(file);
		      ObjectOutput output = new ObjectOutputStream(buffer);
		    )
		    {
		      output.writeObject(this);
		      System.out.println("LCS saved");
		    }  
		    catch(IOException ex){
		    	System.out.println(ex.getMessage());
		    }
	}
	
	public static LCS load()
	{
		try(
		      InputStream file = new FileInputStream("LCS.obj");
		      InputStream buffer = new BufferedInputStream(file);
		      ObjectInput input = new ObjectInputStream (buffer);
		    ){
		      LCS result = (LCS)input.readObject();
		      if ( result != null )
		      {
		    	  System.out.println("LCS loaded");
		    	  return result;
		      }
		    	  
		    }
		    catch(ClassNotFoundException ex){
		    	System.out.println(ex.getMessage());
		    }
		    catch(IOException ex){
		    	System.out.println(ex.getMessage());
		    }
	    	return null;
	}
}
